package br.mikhas.compiler.scanner;

import static br.mikhas.compiler.scanner.TokenType.*;

import java.util.ArrayList;
import java.util.List;

public class Scanner {

	private String source;

	private InternalState internalState;

	private class InternalState {
		int position = 0;
		int line = 1;
		boolean inString = false;
		Token lastToken = null;
		List<Token> tolkens = new ArrayList<Token>();
	}

	class Location {
		final int start;
		final int end;
		final int line;

		public Location(int start, int end, int line) {
			this.start = start;
			this.end = end;
			this.line = line;
		}
	}

	class Token {
		final TokenType type;
		final Location location;
		final String text;

		public Token() {
			this.text = null;
			this.location = null;
			this.type = null;
		}

		public Token(TokenType type, int start, int end) {
			this.text = source.substring(start, end);
			this.location = new Location(start, end, internalState.line);
			this.type = type;
		}
	}

	public Scanner(String source) {
		this.source = source;
		internalState = new InternalState();
	}

	public List<Token> scan() {
		internalState.lastToken = new Token();
		internalState.tolkens.add(internalState.lastToken);
		try {
			int start;
			int end;
			Token token;
			do {
				skipWhiteSpace();
				start = position();
				TokenType type = scanTolken();
				end = position();
				token = new Token(type, start, end);
				internalState.tolkens.add(token);
				internalState.lastToken = token;
			} while (token != null);
		} catch (EofException e) {
		}
		return internalState.tolkens.subList(1, internalState.tolkens.size());
	}

	private TokenType scanTolken() {
		switch (look()) {
		case '"':
			return scanString();
		case '<':
			// < <= << <<=
			next();
			if (is('='))
				return select(LTE);
			if (is('<'))
				return select('=', ASSIGN_SHL, SHL);
			return LT;

		case '>':
			// > >= >> >>=
			next();
			if (is('='))
				return select(GTE);
			if (is('>')) {
				// >> >>=
				next();
				if (is('='))
					return select(ASSIGN_SAR);
				return SAR;
			}
			return GT;

		case '=':
			// = == === =>
			next();
			if (is('='))
				return select('=', EQ_STRICT, EQ);
			return ASSIGN;

		case '!':
			// ! != !==
			next();
			if (is('='))
				return select('=', NE_STRICT, NE);
			return NOT;

		case '+':
			// + ++ +=
			next();
			if (is('+'))
				return select(INC);
			if (is('='))
				return select(ASSIGN_ADD);
			return ADD;

		case '-':
			// - -- -=
			next();
			if (is('-'))
				return select(DEC);
			if (is('='))
				return select(ASSIGN_SUB);
			return SUB;

		case '*':
			// * *=
			return select('=', ASSIGN_MUL, MUL);

		case '%':
			// % %=
			return select('=', ASSIGN_MOD, MOD);

		case '/':
			// / // /* /=
			next();
			// if (is('/'))
			// return skipSingleLineComment();
			// if (is('*'))
			// return skipMultiLineComment();
			if (is('='))
				return select(ASSIGN_DIV);
			return DIV;

		case '&':
			// & && &=
			next();
			if (is('&'))
				return select(AND);
			if (is('='))
				return select(ASSIGN_BIT_AND);
			return BIT_AND;

		case '|':
			// | || |=
			next();
			if (is('|'))
				return select(OR);
			if (is('='))
				return select(ASSIGN_BIT_OR);
			return BIT_OR;

		case '^':
			// ^ ^=
			return select('=', ASSIGN_BIT_XOR, BIT_XOR);

		case '.':
			next();
			return PERIOD;

		case ':':
			return select(COLON);

		case ';':
			return select(SEMICOLON);

		case ',':
			return select(COMMA);

		case '(':
			return select(LPAREN);

		case ')':
			return select(RPAREN);

		case '[':
			next();
			if (is(']'))
				return INDEX;
			return LBRACK;

		case ']':
			return select(RBRACK);

		case '{':
			return select(LBRACE);

		case '}':
			return select(RBRACE);

		case '?':
			return select(CONDITIONAL);

		case '~':
			// ~ ~/ ~/=
			next();
			if (is('/')) {
				next();
				if (is('=')) {
					next();
					return select(ASSIGN_TRUNC);
				}
				return select(TRUNC);
			} else {
				return BIT_NOT;
			}

		default:
			return null;
		}
	}

	private TokenType select(int next, TokenType yes, TokenType no) {
		next();
		if (look() != next)
			return no;
		next();
		return yes;
	}

	private TokenType select(TokenType token) {
		next();
		return token;
	}

	private TokenType scanString() {
		assert is('"');

		next();
		while (!is('"')) {
			next();
		}
		next();

		return STRING_LITERAL;
	}

	private char look() {
		try {
			return source.charAt(internalState.position);
		} catch (StringIndexOutOfBoundsException e) {
			throw new EofException();
		}
	}

	private boolean is(char c) {
		try {
			return source.charAt(internalState.position) == c;
		} catch (StringIndexOutOfBoundsException e) {
			throw new EofException();
		}
	}

	private int position() {
		return internalState.position;
	}

	private void next() {
		if (is('\n'))
			internalState.line++;
		internalState.position++;
	}

	private void skipWhiteSpace() {
		if (internalState.inString)
			return;
		while (true) {
			char c = look();
			if (!(isWhiteSpace(c) || isLineBreak(c))) {
				break;
			}
			next();
		}
	}

	private boolean isWhiteSpace(char c) {
		return c == '\t' || c == ' ';
	}

	private boolean isLineBreak(char c) {
		return c == '\r' || c == '\n';
	}
}
